﻿// "Therefore those skilled at the unorthodox
// are infinite as heaven and earth,
// inexhaustible as the great rivers.
// When they come to an end,
// they bagin again,
// like the days and months;
// they die and are reborn,
// like the four seasons."
// 
// - Sun Tsu,
// "The Art of War"

using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Reflection;

namespace Infrastructure.Logging
{
    /// <summary>
    /// Class used to log operation.
    /// 
    /// <para>
    /// Execution Logging:
    /// ------------------
    /// the measurement starts in ctor and ends in dispose of the object where the measurement
    /// results are written to log.
    /// Operation name is written to "Operation" ThreadContext property.
    /// Operation execution measurement is written to "ExecutionTime" ThreadContext property.
    /// </para>
    /// 
    /// User example:
    /// <code>
    /// using(OperationDC opp = new OperationDC( "[Operation name]" ) ) // start the measurement
    /// {
    ///     ...
    ///     opp.State = true;
    ///     ...
    /// } // stop the measurement and log the results
    /// </code>
    /// 
    /// <para>
    /// Atomic Operation Logging:
    /// ------------------------
    /// Additionaly to normal operation logging the class exposes static function
    /// to log operation in single method call, in with the execution time will be null.
    /// </para>
    /// 
    /// <para>
    /// Note that if OperationGuidLogger was used in the calss stack of the OperationDC,
    /// then the OperationDC log will contain the GUID as well.
    /// It is advisable for ITIL >__<
    /// </para>
    /// 
    /// </summary>
    /// 
    public class OperationDC : IDisposable
    {
        #region Data members

        /// <summary>
        /// logger
        /// </summary>
        private static readonly ILog log = LogManager.GetLogger(MethodBase.GetCurrentMethod().DeclaringType);

        private const string OPERATION = "Operation";
        private const string SUBOPERATION = "SubOperation";
        private const string IS_SUCCESSFUL = "Successful";
        private const string OPERATION_DATA = "OperationData";
        private const string EXECUTION_TIME = "ExecutionTime";
        private const string MESSAGE = "Operation:({0}; {1}; {2}), Successful:({3}), Time:[{4} msec]";

        /// <summary>
        /// the name of the operation
        /// </summary>
        private readonly string mOperation;

        /// <summary>
        /// the name of the sub operation, if the operation has more than one flavor.
        /// </summary>
        private string mSubOperation;

        /// <summary>
        /// stopwatch to measure the operation execution time
        /// </summary>
        private readonly Stopwatch mStopWatch;

        /// <summary>
        /// the state of the operation
        /// </summary>
        private bool mIsSuccessful;

        /// <summary>
        /// flag to indicate if the object has been disposed
        /// </summary>
        private bool mIsDisposed;

        /// <summary>
        /// list to hold the operation data collected
        /// </summary>
        private List<string> mOperationDataList;

        /// <summary>
        /// list of key value pair for extended data.
        /// key - the name of the data to put in the thread context in logging.
        /// value - the value of the data to put in the thread context in logging.
        /// </summary>
        private List<KeyValuePair<string, string>> mExtendedDataList;

        #endregion


        #region Ctors

        /// <summary>
        /// Create new operation logger instance for the given operation name.
        /// start measuring the operation execution time.
        /// Sub operation is set to null.
        /// Initial state set to false.
        /// </summary>
        /// <param name="operation">the operation name to measure</param>
        public OperationDC(string operation)
                : this(operation, null)
        { }

        /// <summary>
        /// Create new operation logger instance for the given operation name and sub name.
        /// start measuring the operation execution time.
        /// Initial state set to false.
        /// </summary>
        /// <param name="operation">the operation name to measure</param>
        /// <param name="subOperation">the name of the sub operation, 
        /// if the operation has more than one flavor</param>
        public OperationDC(string operation, string subOperation)
                : this(operation, subOperation, false)
        { }

        /// <summary>
        /// Create new operation logger instance for the given operation name.
        /// start measuring the operation execution time.
        /// Sub operation is set to null.
        /// </summary>
        /// <param name="operation">the operation name to measure</param>
        /// <param name="initialState">the initial state of the operation (success\failure)</param>
        public OperationDC(string operation, bool initialState)
                : this(operation, null, initialState)
        { }

        /// <summary>
        /// Create new operation logger instance for the given operation name and sub name.
        /// start measuring the operation execution time.
        /// </summary>
        /// <param name="operation">the operation name to measure</param>
        /// <param name="subOperation">the name of the sub operation, 
        /// if the operation has more than one flavor</param>
        /// <param name="initialState">the initial state of the operation (success\failure)</param>
        public OperationDC(string operation, string subOperation, bool initialState)
        {
            if (operation == null) throw new ArgumentNullException("operation");
            if (operation.Equals(String.Empty)) throw new ArgumentException("Empty String", "operation");

            mOperation = operation;
            mSubOperation = subOperation;
            mIsSuccessful = initialState;

            mStopWatch = new Stopwatch();
            mStopWatch.Start();
        }

        #endregion


        /// <summary>
        /// the name of the sub operation, if the operation has more than one flavor.
        /// </summary>
        public string SubOperation
        {
            get { return mSubOperation; }
            set { mSubOperation = value; }
        }

        /// <summary>
        /// Get the elapsed miliseconds since the creation of the object.
        /// </summary>
        public long ElapsedMilliseconds
        {
            get { return mStopWatch.ElapsedMilliseconds; }
        }

        /// <summary>
        /// Set the success state of the operation.
        /// </summary>
        public bool Successful
        {
            get { return mIsSuccessful; }
            set { mIsSuccessful = value; }
        }

        /// <summary>
        /// Cancel operation logging, NO LOG will be writen on dispose.
        /// </summary>
        public void Cancel()
        {
            // setting the dispose flag to true will have the effect of not logging thr op.
            mIsDisposed = true;
            mStopWatch.Stop();
        }

        /// <summary>
        /// Stop the measurement of time, resume is possible through ResumeTimer.
        /// The elapsed time from start to execution of
        /// this stop method will be writen to log if and when the dispose\close is called.
        /// </summary>
        public void StopTimer()
        {
            mStopWatch.Stop();
        }

        /// <summary>
        /// Resume the measurement of time after 'StopTimer' was done.
        /// </summary>
        public void ResumeTimer()
        {
            mStopWatch.Start();
        }

        /// <summary>
        /// Add the given data to the operation data collection.
        /// the data string should be given as a single string.
        /// all the added data are added to create single string, each data
        /// string is seperated by white space.
        /// NOTE: if the data value is null or empty it's ignored without error.
        /// </summary>
        /// <param name="data">operation data string to add</param>
        public void AddOperationData(object data)
        {
            if( data == null )
                return;

            if (mOperationDataList == null)
                mOperationDataList = new List<string>();

            mOperationDataList.Add( data.ToString() );
        }

        /// <summary>
        /// Add the given data to the operation data collection.
        /// the data string should be given as a single string.
        /// all the added data are added to create single string, each data
        /// string is seperated by white space.
        /// NOTE: if the data value is null or empty it's ignored without error.
        /// </summary>
        /// <param name="data">operation data string to add</param>
        public void AddOperationData(params string[] data)
        {
            if (data == null)
                return;

            if (mOperationDataList == null)
                mOperationDataList = new List<string>();

            foreach (string sData in data)
                mOperationDataList.Add(sData);
        }

        /// <summary>
        /// 
        /// NOTE: if the data name or value is null or empty it's ignored without error.
        /// </summary>
        /// <param name="name">the name of the data to put in the thread context in logging</param>
        /// <param name="data">the value of the data to put in the thread context in logging</param>
        public void AddExtendedData(string name, string data)
        {
            if (string.IsNullOrEmpty(name) || string.IsNullOrEmpty(data))
                return;

            if (mExtendedDataList == null)
                mExtendedDataList = new List<KeyValuePair<string, string>>();

            mExtendedDataList.Add(new KeyValuePair<string, string>(name, data));
        }

        /// <summary>
        /// Stop the operation measurement and write the results to log.
        /// Identical to dispose. (safe to call multiple times)
        /// </summary>
        public void Finish()
        {
            Dispose();
        }

        /// <summary>
        /// Stop the operation measurement and write the results to log.
        /// </summary>
        public void Dispose()
        {
            if (!mIsDisposed)
            {
                mIsDisposed = true;
                mStopWatch.Stop();
                long executionTime = mStopWatch.ElapsedMilliseconds > 0
                                             ? mStopWatch.ElapsedMilliseconds
                                             : 1;
                LogOperation( mOperation,
                              mSubOperation,
                              mIsSuccessful,
                              executionTime,
                              CreateOperationDataString(),
                              mExtendedDataList );
            }
        }


        #region Static Atomic Operation logging

        /// <summary>
        /// Log atomic operation, the operation is loged as successful without execution time.
        /// </summary>
        /// <param name="operation">the operation name</param>
        public static void LogAtomicOperation(string operation)
        {
            LogOperation(operation, null, true, 0, null, null);
        }

        /// <summary>
        /// Log atomic operation, the operation is loged as successful without execution time.
        /// </summary>
        /// <param name="operation">the operation name</param>
        /// <param name="subOperation">the name of the sub operation, 
        /// if the operation has more than one flavor</param>
        public static void LogAtomicOperation(string operation, string subOperation)
        {
            LogOperation(operation, subOperation, true, 0, null, null);
        }

        /// <summary>
        /// Log atomic operation, the operation is loged without execution time.
        /// </summary>
        /// <param name="operation">the operation name</param>
        /// <param name="state">the state of the operation (successful \ failed)</param>
        public static void LogAtomicOperation(string operation, bool state)
        {
            LogOperation(operation, null, state, 0, null, null);
        }

        /// <summary>
        /// Log atomic operation, the operation is loged without execution time.
        /// </summary>
        /// <param name="operation">the operation name</param>
        /// <param name="subOperation">the name of the sub operation, 
        /// if the operation has more than one flavor</param>
        /// <param name="state">the state of the operation (successful \ failed)</param>
        public static void LogAtomicOperation(string operation, string subOperation,
                                              bool state)
        {
            LogOperation(operation, subOperation, state, 0, null, null);
        }

        /// <summary>
        /// Log atomic operation, the operation is loged as successful without execution time.
        /// </summary>
        /// <param name="operation">the operation name</param>
        /// <param name="executionTime">the time it took to execute the operation</param>
        public static void LogAtomicOperation(string operation, long executionTime)
        {
            LogOperation(operation, null, true, executionTime, null, null);
        }

        /// <summary>
        /// Log atomic operation, the operation is loged as successful without execution time.
        /// </summary>
        /// <param name="operation">the operation name</param>
        /// <param name="subOperation">the name of the sub operation, 
        /// if the operation has more than one flavor</param>
        /// <param name="executionTime">the time it took to execute the operation</param>
        public static void LogAtomicOperation(string operation, string subOperation, long executionTime)
        {
            LogOperation(operation, subOperation, true, executionTime, null, null);
        }

        /// <summary>
        /// Log atomic operation, the operation is loged without execution time.
        /// </summary>
        /// <param name="operation">the operation name</param>
        /// <param name="state">the state of the operation (successful \ failed)</param>
        /// <param name="executionTime">the time it took to execute the operation</param>
        public static void LogAtomicOperation(string operation, bool state, long executionTime)
        {
            LogOperation(operation, null, state, executionTime, null, null);
        }

        /// <summary>
        /// Log atomic operation, the operation is loged without execution time.
        /// </summary>
        /// <param name="operation">the operation name</param>
        /// <param name="subOperation">the name of the sub operation, 
        /// if the operation has more than one flavor</param>
        /// <param name="state">the state of the operation (successful \ failed)</param>
        /// <param name="executionTime">the time it took to execute the operation</param>
        public static void LogAtomicOperation(string operation, string subOperation,
                                              bool state, long executionTime)
        {
            LogOperation(operation, subOperation, state, executionTime, null, null);
        }

        /// <summary>
        /// Log atomic operation, the operation is loged as successful without execution time.
        /// </summary>
        /// <param name="operation">the operation name</param>
        /// <param name="oppData">operation data string</param>
        public static void LogAtomicOperationData(string operation, string oppData)
        {
            LogOperation(operation, null, true, 0, oppData, null);
        }

        /// <summary>
        /// Log atomic operation, the operation is loged as successful without execution time.
        /// </summary>
        /// <param name="operation">the operation name</param>
        /// <param name="subOperation">the name of the sub operation, 
        /// if the operation has more than one flavor</param>
        /// <param name="oppData">operation data string</param>
        public static void LogAtomicOperationData(string operation, string subOperation, string oppData)
        {
            LogOperation(operation, subOperation, true, 0, oppData, null);
        }

        /// <summary>
        /// Log atomic operation, the operation is loged without execution time.
        /// </summary>
        /// <param name="operation">the operation name</param>
        /// <param name="oppData">operation data string</param>
        /// <param name="state">the state of the operation (successful \ failed)</param>
        public static void LogAtomicOperationData(string operation, string oppData, bool state)
        {
            LogOperation(operation, null, state, 0, oppData, null);
        }

        /// <summary>
        /// Log atomic operation, the operation is loged without execution time.
        /// </summary>
        /// <param name="operation">the operation name</param>
        /// <param name="subOperation">the name of the sub operation, 
        /// if the operation has more than one flavor</param>
        /// <param name="oppData">operation data string</param>
        /// <param name="state">the state of the operation (successful \ failed)</param>
        public static void LogAtomicOperationData(string operation, string subOperation,
                                                  string oppData, bool state)
        {
            LogOperation(operation, subOperation, state, 0, oppData, null);
        }

        /// <summary>
        /// Log atomic operation, the operation is loged as successful without execution time.
        /// </summary>
        /// <param name="operation">the operation name</param>
        /// <param name="oppData">operation data string</param>
        /// <param name="executionTime">the time it took to execute the operation</param>
        public static void LogAtomicOperationData(string operation, string oppData, long executionTime)
        {
            LogOperation(operation, null, true, executionTime, oppData, null);
        }

        /// <summary>
        /// Log atomic operation, the operation is loged as successful without execution time.
        /// </summary>
        /// <param name="operation">the operation name</param>
        /// <param name="subOperation">the name of the sub operation, 
        /// if the operation has more than one flavor</param>
        /// <param name="oppData">operation data string</param>
        /// <param name="executionTime">the time it took to execute the operation</param>
        public static void LogAtomicOperationData(string operation, string subOperation,
                                                  string oppData, long executionTime)
        {
            LogOperation(operation, subOperation, true, executionTime, oppData, null);
        }

        /// <summary>
        /// Log atomic operation, the operation is loged without execution time.
        /// </summary>
        /// <param name="operation">the operation name</param>
        /// <param name="oppData">operation data string</param>
        /// <param name="state">the state of the operation (successful \ failed)</param>
        /// <param name="executionTime">the time it took to execute the operation</param>
        public static void LogAtomicOperationData(string operation, string oppData,
                                                  bool state, long executionTime)
        {
            LogOperation(operation, null, state, executionTime, oppData, null);
        }

        /// <summary>
        /// Log atomic operation, the operation is loged without execution time.
        /// </summary>
        /// <param name="operation">the operation name</param>
        /// <param name="subOperation">the name of the sub operation, 
        /// if the operation has more than one flavor</param>
        /// <param name="oppData">operation data string</param>
        /// <param name="state">the state of the operation (successful \ failed)</param>
        /// <param name="executionTime">the time it took to execute the operation</param>
        public static void LogAtomicOperationData(string operation, string subOperation,
                                                  string oppData, bool state, long executionTime)
        {
            LogOperation(operation, subOperation, state, executionTime, oppData, null);
        }

        #endregion


        #region Private methods

        /// <summary>
        /// Log the operation.
        /// Set lock on the class so that no two operation can log at the same time.
        /// </summary>
        /// <param name="operation">the name of the operation</param>
        /// <param name="subOperation">the name of the sub operation, 
        /// if the operation has more than one flavor</param>
        /// <param name="state">the state of the operation</param>
        /// <param name="executionTime">the time it took to execute the operation</param>
        /// <param name="operationData">operation data string</param>
        /// <param name="extData">operation extended data</param>
        private static void LogOperation(string operation, string subOperation,
                                         bool? state, long executionTime,
                                         string operationData,
                                         IEnumerable<KeyValuePair<string, string>> extData)
        {
            try
            {
                ThreadContext.Properties[OPERATION] = operation;
                ThreadContext.Properties[SUBOPERATION] = subOperation;
                ThreadContext.Properties[IS_SUCCESSFUL] = state;
                if (executionTime > 0)
                    ThreadContext.Properties[EXECUTION_TIME] = executionTime;
                if (operationData != null)
                    ThreadContext.Properties[OPERATION_DATA] = operationData;
                if (extData != null)
                    foreach (KeyValuePair<string, string> pair in extData)
                        ThreadContext.Properties[pair.Key] = pair.Value;

                log.InfoFormat(MESSAGE, operation, subOperation, operationData, state, executionTime);
            }
            finally
            {
                if (extData != null)
                    foreach (KeyValuePair<string, string> pair in extData)
                        ThreadContext.Properties[pair.Key] = null;
                ThreadContext.Properties[OPERATION_DATA] = null;
                ThreadContext.Properties[EXECUTION_TIME] = null;
                ThreadContext.Properties[IS_SUCCESSFUL] = null;
                ThreadContext.Properties[SUBOPERATION] = null;
                ThreadContext.Properties[OPERATION] = null;
            }
        }

        /// <summary>
        /// Create operation data string from operation data collection.
        /// </summary>
        /// <returns>the created string</returns>
        private string CreateOperationDataString()
        {
            if (mOperationDataList == null)
                return null;

            string oppStr = string.Empty;
            foreach (string opp in mOperationDataList)
                oppStr = string.Format("{0} {1}", oppStr, opp);
            return oppStr;
        }

        #endregion
    }
}